package org.codesketch.flamingo


import com.codesketch.flamingo.domain.BAN
import com.codesketch.flamingo.domain.Parameter
import com.codesketch.flamingo.domain.VCL
import java.text.DateFormat;
import java.text.SimpleDateFormat

import org.apache.catalina.InstanceEvent;
import org.apache.log4j.Logger;
import org.codesketch.flamingo.domain.Response;
import org.springframework.transaction.annotation.Transactional;

class NodeService {
	Logger logger = Logger.getLogger(NodeService.class)

	@Transactional(readOnly=false)
	def monitor() {
		DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
		// removing contents older than 8hours
		def removed = age()
		logger.debug("Removed ${removed} snapshot")
		// retrieve all registered node
		def nodes = Node.list();
		nodes.each { node ->
			Response<byte[]> resp = node.executeSSHCommand("varnishstat -x");
			if(resp.getStatus()==0) {
				Snapshot snapshot = new Snapshot();
				def records = new XmlSlurper().parse(new ByteArrayInputStream(resp.getData()));
				snapshot.setNodeId(node.id)
				snapshot.setDate(format.parse(records.@timestamp.text().toString()));
				records.stat.each { stat ->
					stat.each { n ->
						if(n.name == 'client_req') {
							snapshot.setRequested(n.value.text().toLong());
						}
						if(n.name == 'client_conn') {
							snapshot.setAccepted(n.value.text().toLong());
						}
						if(n.name == 'client_drop') {
							snapshot.setDropped(n.value.text().toLong());
						}
						if(n.name == 'cache_hit') {
							snapshot.setHit(n.value.text().toLong());
						}
						if(n.name == 'cache_hitpass') {
							snapshot.setHitpass(n.value.text().toLong());
						}
						if(n.name == 'cache_miss') {
							snapshot.setMiss(n.value.text().toLong());
						}
						if(n.name == 'backend_conn') {
							snapshot.setBackendConnection(n.value.text().toLong());
						}
						if(n.name == 'backend_unhealthy') {
							snapshot.setBackendUnhealty(n.value.text().toLong());
						}
						if(n.name == 'backend_busy') {
							snapshot.setBackendBusy(n.value.text().toLong());
						}
						if(n.name == 'backend_fail') {
							snapshot.setBackendFail(n.value.text().toLong());
						}
						if(n.name == 'backend_reuse') {
							snapshot.setBackendReuse(n.value.text().toLong());
						}
						if(n.name == 'backend_toolate') {
							snapshot.setBackendToolate(n.value.text().toLong());
						}
						if(n.name == 'n_lru_nuked') {
							snapshot.setNuked(n.value.text().toLong());
						}
					}
				}
				snapshot.save()
			}
		}
	}
	/**
	 * Retrieve the configuration of all the known nodes.
	 * @return
	 */
	@Transactional(readOnly=true)
	def configuration() {
		def nodes = Node.findAllWhere(active:true);
		def resp = [:]
		nodes.each { node ->
			def vcl = ls(node)
			def param = pshow(node, "")
			def snapshots = Snapshot.findAllByNodeId(node.id, [max:10, sort:'date', order: "desc"])
			resp[node.id] = ['node':node, 'vcl': vcl, 'params': param, 'snapshots': snapshots, 'version': version(node)]
		}
		return resp
	}
	/**
	 * Remove all the snapshot that are older than 8 hours.
	 */
	@Transactional(readOnly=false)
	def age() {
		final Long oldDateMs = System.currentTimeMillis() - (8 * 60 * 60 * 1000)
		logger.info("Removing snapshot stored before ${oldDateMs}")
		return Snapshot.executeUpdate("delete Snapshot s where s.date <= :oldDate", [oldDate: new Date(oldDateMs)]);
	}
	
	// --Node CLI command implementation
	def version(Long id) {
		return version(Node.get(id))
	}
	def version(Node node) {
		logger.info(String.format("Get Varnish version for node {%s}", node.name))
		Response<byte[]> vr = exec(node, "varnishd -V")
		def resp = []
		if(0==vr.getStatus()) {
			BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vr.getData())))
			resp.add(br.readLine())
			br.close();
		} else {
			resp.add("")
		}
		return resp
	}
	
	/**
	* List the VCL file available on the requested node.
	* @param id the node identifier
	* @return a list of VCL if any.
	*/
	def ls(Long id) {
		return ls(Node.get(id))
	}
	/**
	 * List the VCL file available on the requested node.
	 * @param node the node
	 * @return a list of VCL if any.
	 */
	@Transactional(readOnly=true)
	def ls(Node node) {
		logger.info(String.format("List VCL for node {%s}", node.name))
		Response<byte[]> vr = exec(node, String.format("varnishadm -T127.0.0.1:%d vcl.list", node.adminPort))
		def resp = []
		if(0==vr.getStatus()) {
			BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vr.getData())))
			String line;
			while(null != (line=br.readLine())) {
				if("" != line) {
					resp.add(getVCL(line));
				}
			}
		} else {
			resp.add(new VCL(hasError:true, error:new String(vr.getData())))
		}
		return resp
	}
	/**
	 * Load and compile the provided VCL on the requested node.
	 * @param id the node identifier
	 * @param name the VCL name
	 * @param vcl the VCL file content
	 * @return the load and compile outcome.
	 */
	@Transactional(readOnly=true)
	def inline(Long id, String name, String vcl) {
		def node = Node.get(id)
		logger.info(String.format("Upload VCL for node {%s}", node.name))
		// ? create a temporary file and use vcl.load ?
//		String es = vcl.replaceAll("\"", "\\\\\"");
		// use strong quote to quote the vcl
		String command = String.format("varnishadm -T127.0.0.1:%d vcl.inline %s '%s'", node.adminPort, name, vcl)
		def inlineResp = []
		Response<byte[]> vr = exec(node, command)
		if(0==vr.getStatus()) {
			BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vr.getData())))
			String line;
			while(null != (line=br.readLine())) {
				inlineResp.add(line);
			}
		} else {
			inlineResp.add(new String(vr.getData()));
		}
		return inlineResp
	}
	/**
	 * Discard the request VCL from the requested node
	 * @param id the node identifier
	 * @param name the VCL name
	 * @return the discard response
	 */
	@Transactional(readOnly=true)
	def discard(Long id, String name) {
		def node = Node.get(id)
		logger.info(String.format("Discard VCL from node {%s}", node.name))
		def resp = []
		Response<byte[]> vr = exec(node, String.format("varnishadm -T127.0.0.1:%d vcl.discard %s", node.adminPort, name))
		if(0==vr.getStatus()) {
			BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vr.getData())))
			String line;
			while(null != (line=br.readLine())) {
				resp.add(line)
			}
		} else {
			resp.add(new String(vr.getData()))
		}
		return resp
	}
	/**
	 * Set the request VCL as the VCL to use on the requested node
	 * @param id the node identifier
	 * @param name the VCL name
	 * @return the use request outcome
	 */
	@Transactional(readOnly=true)
	def use(Long id, String name) {
		def node = Node.get(id)
		logger.info(String.format("Discard VCL from node {%s}", node.name))
		def resp = []
		Response<byte[]> vr = exec(node, String.format("varnishadm -T127.0.0.1:%d vcl.use %s", node.adminPort, name))
		if(0==vr.getStatus()) {
			BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vr.getData())))
			String line;
			while(null != (line=br.readLine())) {
				resp.add(line)
			}
		} else {
			resp.add(new String(vr.getData()));
		}
		return resp
	}
	/**
	 * Display the source code of the requested VCL
	 * @param id the node identifier
	 * @param name the VCL name
	 * @return the VCL show outcome
	 */
	@Transactional(readOnly=true)
	def show(Long id, String name) {
		def node = Node.get(id)
		logger.info(String.format("Show VCL {%s} from node {%s}", name, node.name))
		def showResp = []
		Response<byte[]> vr = exec(node, String.format("varnishadm -T127.0.0.1:%d vcl.show %s", node.adminPort, name))
		if(0==vr.getStatus()) {
			BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vr.getData())))
			String line;
			while(null != (line=br.readLine())) {
				showResp.add(line)
			}
		} else {
		
		}
		return showResp
	}
	/**
	 * Sets the requested parameter on the requested node
	 * @param id the node identifier
	 * @param name the parameter name
	 * @param value the parameter value
	 * @return the parameter set response
	 */
	@Transactional(readOnly=true)
	def pset(Long id, String name, String value){
		def node = Node.get(id)
		logger.info(String.format("Set parameter {%s = %s} from node {%s}", name, value, node.name))
		def resp = []
		Response<byte[]> vr = exec(node, String.format("varnishadm -T127.0.0.1:%d param.set %s %s", node.adminPort, name, value))
		if(0==vr.getStatus()) {
			BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vr.getData())))
			String line;
			while(null != (line=br.readLine())) {
				System.out.println(line)
			}
		} else {
		
		}
		return resp
	}
	/**
	 * Shows the value of the requested parameter for the requested node
	 * @param id the node identifier
	 * @param name the parameter name
	 * @return the parameter set response
	 */
	def pshow(Long id, String args){
		return pshow(Node.get(id), args);
	}
	/**
	* Shows the value of the requested parameter for the requested node
	* @param node the node
	* @param name the parameter name
	* @return the parameter set response
	*/
	@Transactional(readOnly=true)
	def pshow(Node node, String args){
		logger.info(String.format("Show parameter {%s} from node {%s}", args, node.name))
		def resp = []
		Response<byte[]> vr = exec(node, String.format("varnishadm -T127.0.0.1:%d param.show %s", node.adminPort, args))
		if(0==vr.getStatus()) {
			BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vr.getData())))
			String line;
			while(null != (line=br.readLine())) {
				if("" != line) {
					resp.add(getParameter(line))
				}
			}
		} else {
			resp.add(new Parameter(hasError:true, error:new String(vr.getData())))
		}
		return resp
	}
	/**
	 * Invalidate all documents matching the ban expression. See Varnish Ban Expressions for more documentation and examples.
	 * @param id the node identifier
	 * @param exp the ban expression in the form: "<code>field operator argument [&& field operator argument [...]]</code>"
	 * @return the ban response
	 */
	@Transactional(readOnly=true)
	def ban(Long id, String exp){
		def node = Node.get(id)
		logger.info(String.format("Execute Ban using {%s} on node {%s}", exp, node.name))
		def resp = []
		Response<byte[]> vr = exec(node, String.format("varnishadm -T127.0.0.1:%d ban %s", node.adminPort, exp))
		if(0==vr.getStatus()) {
			BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vr.getData())))
			String line;
			while(null != (line=br.readLine())) {
				System.out.println(line)
			}
		} else {
			
		}
		return resp
	}
	/**
	* Displays the ban list.
	* @param id the node identifier
	* @return the ban list response
	*/
   @Transactional(readOnly=true)
   def blist(Long id){
	   def node = Node.get(id)
	   logger.info(String.format("Listing Ban for node {%s}", node.name))
	   def resp = []
	   Response<byte[]> vr = exec(node, String.format("varnishadm -T127.0.0.1:%d ban.list", node.adminPort))
	   if(0==vr.getStatus()) {
		   BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vr.getData())))
		   String line;
		   while(null != (line=br.readLine())) {
			   resp.add(getBAN(line))
		   }
	   } else {
	   		resp.add(new BAN(hasError:true, error:new String(vr.getData())))
	   }
	   return resp
   }
   /**
   * Immediately invalidate all documents whose URL matches the provided regular expression.
   * @param id the node identifier
   * @param exp the ban URL regular expression
   * @return the ban response
   */
  @Transactional(readOnly=true)
  def burl(Long id, String exp){
	  def node = Node.get(id)
	  logger.info(String.format("Execute Ban using {%s} on node {%s}", exp, node.name))
	  def resp = []
	  Response<byte[]> vr = exec(node, String.format("varnishadm -T127.0.0.1:%d ban.url %s", node.adminPort, exp))
	  if(0==vr.getStatus()) {
		  BufferedReader br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(vr.getData())))
		  String line;
		  while(null != (line=br.readLine())) {
			  System.out.println(line)
		  }
	  } else {
	  	
	  }
	  return resp
  }
  
	// -- Helper methods

	/**
	 * Execute the given command on the requested node
	 * @param node the node
	 * @param command the command to execute
	 * @return the command execution response
	 */
	Response<byte[]> exec(Node node, String command) {
		logger.debug(command);
		return node.executeSSHCommand(command)
	}
	
	BAN getBAN(String line) {
		String[] p = line.split("(?<![~&])\\s(?![~&])");
		if(p.length == 4) {
			return new BAN(address:p[0], time:p[1], objects:p[2], ban:p[3]);
		} else {
			return new BAN(hasError:true, error:line);
		}
	}
	
	VCL getVCL(String line) {
		String[] parts = line.split("\\s+")
		if(parts.length==3) {
			return new VCL(identifier:parts[1], status:parts[0], name:parts[2])
		} else {
			logger.warn(String.format("Got an invalid response line {%s}", line))
			return new VCL(hasError:true, error:line)
		}
	}
	
	Parameter getParameter(String line) {
		String[] parts = line.split("\\s+(?=[\\s\"\\[\\(])")
		if(parts.length==3) {
			return new Parameter(name:parts[0].trim(), value: parts[1].trim(), info:parts[2].trim())
		} else if(parts.length==2) {
			// listen address
			return new Parameter(name:parts[0].trim(), value: parts[1].trim(), info:[])
		} else {
			logger.warn(String.format("Got an invalid response line {%s}", line))
			return new Parameter(hasError:true, error:line)
		}
	}
}